using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Text;

#if !PocketPC
using System.Reflection.Emit;
#endif

namespace ODX.Core
{
    internal static class ProxyBuilder
    {
        internal const BindingFlags PropertyFilter =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;

        static internal StringBuilder sb = new StringBuilder();


        private static Type BuildProxy(Session session, Type type)
        {
            if (!type.IsSubclassOf(typeof(Entity)))
                return null;
            if (!type.IsAbstract)
                return null;
            if (BuilderUtils.IsManualClass(type))
                return null;

            return GetProxyType(session, type);
        }

        internal static void BuildProxy(Session session)
        {
            foreach ( Type type in session.Pm.GetTypes() )
            {
                Type proxyType = BuildProxy(session, type);
                if ( proxyType != null )
                    session.RegisterType(proxyType);
            }

            //using ( StreamWriter sw = new StreamWriter("c:\\qqq.cs") )
            //    sw.Write(sb.ToString());
            
        }

        private static Type GetProxyType(Session session, Type type)
        {
            string typeID = type.GUID.ToString("N");
            AssemblyName asmName = new AssemblyName();
            asmName.Name = "ODXProxy_" + typeID;

            AssemblyBuilder asmBuilder =
                AppDomain.CurrentDomain.DefineDynamicAssembly(
                    asmName,
                    AssemblyBuilderAccess.Run);

            ModuleBuilder asmMolule = asmBuilder.DefineDynamicModule("ProxyMdl" + typeID);

            TypeBuilder xClass = asmMolule.DefineType(type.Name + "Proxy", TypeAttributes.Public);
            xClass.SetParent(type);

            sb.AppendFormat("public class {0} : {1}\n", type.Name + "Proxy", type.FullName);
            sb.Append("{\n");

            while (type.IsSubclassOf(typeof (Entity)))
            {
                foreach (PropertyInfo pi in type.GetProperties(PropertyFilter))
                {
                    if (!BuilderUtils.IsAutoProperty(pi))
                        continue;

                    if (pi.PropertyType.IsSubclassOf(typeof (Entity)))
                        CreateParent(xClass, pi, session);
                    else if (new ArrayList(pi.PropertyType.GetInterfaces()).Contains(typeof (IEntityListBase)))
                        CreateList(xClass, pi, session);
                    else
                        CreateAtom(xClass, pi);
                }

                type = type.BaseType;
            }
            sb.Append("}\n\n\n\n\n");

            return xClass.CreateType();
        }

        private static MethodBuilder BuildGetProperty(PropertyInfo pi, TypeBuilder tb)
        {
            return tb.DefineMethod(
                pi.GetGetMethod(true).Name,
                pi.GetGetMethod(true).Attributes & ~MethodAttributes.Abstract,
                pi.PropertyType,
                new Type[] { });
        }

        private static MethodBuilder BuildSetProperty(PropertyInfo pi, TypeBuilder tb)
        {
            return tb.DefineMethod(
                pi.GetSetMethod(true).Name,
                pi.GetSetMethod(true).Attributes & ~MethodAttributes.Abstract,
                null,
                new Type[] { pi.PropertyType });
        }

        private static MethodInfo GetCastMethod(Type type)
        {
            if (type.IsEnum)
                type = typeof (int);
            foreach (MethodInfo mi in typeof(Cast).GetMethods())
                if (mi.ReturnType == type && mi.Name.StartsWith("To"))
                    return mi;
            throw new OdxException("There is no cast defenition!!!");
        }

        private static void CreateAtom(TypeBuilder tb, PropertyInfo pi)
        {
            string columnName = BuilderUtils.GetColumnName(pi);
            ILGenerator il;

            MethodInfo castMethod = GetCastMethod(pi.PropertyType);

            if (pi.GetGetMethod(true) != null)
            {
                MethodBuilder get_Prop = BuildGetProperty(pi, tb);

                il = get_Prop.GetILGenerator();
                il.Emit(OpCodes.Ldarg, 0);
                il.Emit(OpCodes.Ldstr, columnName);
                il.Emit(OpCodes.Call, typeof (Entity).GetMethod("GetProperty", PropertyFilter, null, new Type[] {typeof (string)}, null));
                il.Emit(OpCodes.Call, castMethod);
                il.Emit(OpCodes.Ret);
                tb.DefineMethodOverride(get_Prop, pi.GetGetMethod(true));
            }

            if (pi.GetSetMethod(true) != null)
            {
                MethodBuilder set_Prop = BuildSetProperty(pi, tb);

                il = set_Prop.GetILGenerator();
                il.Emit(OpCodes.Ldarg, 0);
                il.Emit(OpCodes.Ldstr, columnName);
                il.Emit(OpCodes.Ldarg, 1);
                if ( pi.PropertyType == typeof(Guid) )
                    il.Emit(OpCodes.Call, typeof(Cast).GetMethod("FromGuid"));
                else if (pi.PropertyType.IsValueType)
                    il.Emit(OpCodes.Box, pi.PropertyType);
                il.Emit(OpCodes.Call,
                        typeof(Entity).GetMethod("SetProperty", PropertyFilter, null, new Type[] { typeof(string), typeof(object) }, null));
                il.Emit(OpCodes.Ret);
                tb.DefineMethodOverride(set_Prop, pi.GetSetMethod(true));
            }

            sb.AppendFormat("public override {0} {1}\n", pi.PropertyType.FullName, pi.Name);
            sb.Append("\n{\n");
            sb.Append("get{");
            sb.AppendFormat("return ODX.Core.Cast.{0}(GetProperty(\"{1}\"));", castMethod.Name, columnName);
            sb.Append("}\n");
            sb.Append("set{");
            sb.AppendFormat("SetProperty(\"{0}\", value);", columnName);
            sb.Append("}\n");
            sb.Append("}\n\n");
        }

        private static void CreateList(TypeBuilder tb, PropertyInfo pi, Session session)
        {
            LinkTableAttribute[] linkAttrs = (LinkTableAttribute[])pi.GetCustomAttributes(typeof(LinkTableAttribute), false);

            if (linkAttrs.Length > 0)
            {
                CreateLink(linkAttrs[0].Name, linkAttrs[0].ParentColumn, linkAttrs[0].ChildColumn, pi, session, tb);
            }
            else
            {
                string colName = BuilderUtils.GetColumnName(pi);
                if (colName == null)
                {
                    CreateLink(null, null, null, pi, session, tb);
                }
                else
                {
                    string tableName = BuilderUtils.GetRelatedTableName(pi, session.Pm);
                    CreateChildList(tb, pi, BuilderUtils.FindRelation(session.Schema, tableName, colName));
                }
            }
        }

        private static void CreateLink(string linkTableName, string parentColName, string childColName, PropertyInfo pi, Session session, TypeBuilder tb)
        {
            string parentTable = session.Pm.GetTypeTable(pi.ReflectedType);
            string childTable = session.Pm.GetTypeTable(pi.PropertyType.GetGenericArguments()[0]);
            string parentFK = parentColName ?? parentTable + "ID";
            string childFK = childColName ?? childTable + "ID";

            if (linkTableName == null)
                linkTableName = parentTable.CompareTo(childTable) > 0 ? parentTable + childTable : childTable + parentTable;

            CreateAssocList(tb, pi, 
                            BuilderUtils.FindRelation(session.Schema, linkTableName, parentFK),
                            BuilderUtils.FindRelation(session.Schema, linkTableName, childFK));
        }

        private static void CreateAssocList(TypeBuilder tb, PropertyInfo pi, string p2l, string l2ch)
        {
            Type[] genArg = pi.PropertyType.GetGenericArguments();
            if (genArg.Length == 0)
                genArg = new Type[] { typeof(Entity) };

            ConstructorInfo ci = typeof(EntityAssocList<>).MakeGenericType(genArg).GetConstructor(new Type[] { typeof(Entity), typeof(string), typeof(string) });

            MethodBuilder get_Prop = BuildGetProperty(pi, tb);

            ILGenerator il;

            il = get_Prop.GetILGenerator();
            il.Emit(OpCodes.Ldarg, 0);
            il.Emit(OpCodes.Ldstr, p2l);
            il.Emit(OpCodes.Ldstr, l2ch);
            il.Emit(OpCodes.Newobj, ci);
            il.Emit(OpCodes.Ret);
            tb.DefineMethodOverride(get_Prop, pi.GetGetMethod(true));

            sb.AppendFormat("public override ODX.Core.IEntityList<{0}> {1}\n", genArg[0].FullName, pi.Name);
            sb.Append("\n{\n");

            sb.Append("get{");
            sb.AppendFormat("return new ODX.Core.EntityAssocList<{0}>(this, \"{1}\", \"{2}\");", genArg[0].FullName, p2l, l2ch);
            sb.Append("}\n");

            sb.Append("}\n\n");
        }

        private static void CreateChildList(TypeBuilder tb, PropertyInfo pi, string relationName)
        {
            Type[] genArg = pi.PropertyType.GetGenericArguments();
            if (genArg.Length == 0)
                genArg = new Type[] { typeof(Entity) };

            ConstructorInfo ci = typeof(EntityChildList<>).MakeGenericType(genArg).GetConstructor(new Type[] { typeof(Entity), typeof(string) });

            MethodBuilder get_Prop = BuildGetProperty(pi, tb);

            ILGenerator il;

            il = get_Prop.GetILGenerator();
            il.Emit(OpCodes.Ldarg, 0);
            il.Emit(OpCodes.Ldstr, relationName);
            il.Emit(OpCodes.Newobj, ci);
            il.Emit(OpCodes.Ret);
            tb.DefineMethodOverride(get_Prop, pi.GetGetMethod(true));

            sb.AppendFormat("public override ODX.Core.IEntityList<{0}> {1}\n", genArg[0].FullName, pi.Name);
            sb.Append("\n{\n");

            sb.Append("get{");
            sb.AppendFormat("return new ODX.Core.EntityChildList<{0}>(this, \"{1}\");", genArg[0].FullName, relationName);
            sb.Append("}\n");

            sb.Append("}\n\n");
        }

        private static void CreateParent(TypeBuilder tb, PropertyInfo pi, Session session)
        {
            string relationName = BuilderUtils.GetRelationName(pi, session);
            ILGenerator il;

            if (pi.GetGetMethod(true) != null)
            {
                MethodBuilder get_Prop = BuildGetProperty(pi, tb);

                il = get_Prop.GetILGenerator();
                il.Emit(OpCodes.Ldarg, 0);
                il.Emit(OpCodes.Ldstr, relationName);
                il.Emit(OpCodes.Call,
                        typeof (Entity).GetMethod("GetParent", PropertyFilter, null, new Type[] {typeof (string)}, null));
                il.Emit(OpCodes.Castclass, pi.PropertyType);
                il.Emit(OpCodes.Ret);
                tb.DefineMethodOverride(get_Prop, pi.GetGetMethod(true));
            }

            if (pi.GetSetMethod(true) != null)
            {
                MethodBuilder set_Prop = BuildSetProperty(pi, tb);

                il = set_Prop.GetILGenerator();
                il.Emit(OpCodes.Ldarg, 0);
                il.Emit(OpCodes.Ldstr, relationName);
                il.Emit(OpCodes.Ldarg, 1);
                il.Emit(OpCodes.Call,
                        typeof (Entity).GetMethod("SetParent", PropertyFilter, null,
                                                  new Type[] {typeof (string), typeof (Entity)}, null));
                il.Emit(OpCodes.Ret);
                tb.DefineMethodOverride(set_Prop, pi.GetSetMethod(true));
            }

            sb.AppendFormat("public override {0} {1}\n", pi.PropertyType.FullName, pi.Name);
            sb.Append("\n{\n");

            sb.Append("get{");
            sb.AppendFormat("return GetParent(\"{0}\");", relationName);
            sb.Append("}\n");

            sb.Append("set{");
            sb.AppendFormat("SetParent(\"{0}\", value);", relationName);
            sb.Append("}\n");

            sb.Append("}\n\n");
        }
    }
}
